Repository navigation
fix(server): read the A2A profile's input member, and refuse parameters when the profile is activated - #1888
Conversation
…rs when the profile is activated
The default A2A parser read only an invocation's `parameters` member. A client
following the AdCP A2A profile (@adcp/sdk 16.7.0 on any A2A 1.0 agent) sends
`{skill, input}`, so handlers received `{}`: `context` was not echoed and
versioned tools failed with VERSION_UNSUPPORTED.
The parser now reads `parameters` when present, otherwise `input`, and never
merges them, so a caller that does not activate the profile is dispatched
exactly as before. A request that activates the profile (A2A-Extensions, or the
Message's extensions, names https://adcontextprotocol.org/extensions/adcp/v3)
must carry exactly one DataPart with an `input` object; `parameters`, alone or
beside `input`, is refused with INVALID_REQUEST before handler entry, as the
profile requires. The raw-body receipt path reads the same member.
Refs #1887
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… refusal over the real app Refs #1887 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
There was a problem hiding this comment.
Ladon verdict: Approve
Approve — clean, well-scoped profile-conformance fix.
The A2A executor now reads the AdCP A2A profile's input member in addition to parameters (previously profile clients like @adcp/sdk 16.7.0 hit handlers with {}, causing no context echo and VERSION_UNSUPPORTED). _invocation_arguments reads a single member with parameters-wins-else-input precedence and never merges, so non-activated callers are unchanged; _profile_invocation_violation refuses activated requests carrying parameters, missing/non-object input, or ≠1 DataPart before handler entry and echoes no context. The receipt raw-body path reads the same member.
Semver signal is correct (fix:, no public export/field/enum changes — this is profile conformance, not a contract break). 17 HTTP-driven tests cover activation paths, precedence, each refusal shape, custom-parser bypass, and the signature gate.
High-risk flag is true only because src/adcp/server/a2a_server.py (modified) matches src/adcp/server/** — a heuristic LOOK signal, not an escalation trigger. The reviewer found no medium-or-higher concerns on the modified file, so the modification is presumed safe. No gated paths, no no-auto-approve team match, no prior decision.
No blocking or medium findings → decision table falls through to row 9.
…file ADCPAgentExecutor.execute is public, and a context built by hand (an adopter's executor test) may lack RequestContext.requested_extensions or Message.extensions. The profile check now reads both defensively instead of raising AttributeError. Refs #1887 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
There was a problem hiding this comment.
Ladon verdict: Approve
Approve.
Delta since the prior approve is a single defensive change in _profile_invocation_violation (a2a_server.py L643-648): context.requested_extensions and message.extensions are now read via getattr, so the public execute path can no longer raise AttributeError on a hand-built context missing a2a-sdk attributes. The change is correct and fail-closed — absent attributes read as "profile not activated," which only downgrades hand-built contexts and never a wire-activated request (a2a-sdk always populates those fields). The requested ... or () guard covers a None value; the asymmetry vs message.extensions is justified since the real protobuf Message always exposes a list. A new test (tests/test_a2a_profile_input_member.py) covers the branch.
Decision-table walk: no critical/high/medium findings (row 1 n/a). gated_paths is false and review_decision is APPROVED (row 2 n/a). high_risk is true only via src/adcp/server/a2a_server.py (modified) — no (deleted) entry (row 3 n/a). No medium findings, so rows 4/5/8 n/a. Prior decision was approve, not escalate (row 6 n/a). No no-auto-approve team match (row 7 n/a). Falls through to row 9 → approve.
High-risk flag is a modified server file with no medium-or-higher concern — presumed safe per the modification rule. Clean pass.
Part of #1887: the
next(10.x beta) half. The 9.x backport is #1889.Problem
The A2A executor read only an invocation DataPart's
parametersmember. A client that follows the AdCP A2A profile sends{"skill": ..., "input": {...}}.@adcp/sdk16.7.0 does this on any agent whose card advertises A2A 1.0, which this server's card does, so the handler received{}. Two symptoms followed:contextwas not echoed, and versioned tools failed withVERSION_UNSUPPORTED: adcp_version is required.Rule: which member is read
parametersonlyparametersA2AAdapter.inputonlyinputparametersinputonlyinputparameterspresent (alone or besideinput)INVALID_REQUEST, before the handler runs.inputmissing or not an object, or the number of DataParts is not oneINVALID_REQUEST, before the handler runs."Activated" means that the
A2A-Extensionsheader, or the Message's ownextensionslist, nameshttps://adcontextprotocol.org/extensions/adcp/v3.@adcp/sdksends both.The profile isn't silent on the activated rows. It says "
parametersis not an alias forinput" and that a client sendingparametersis non-conformant: "the agent MUST reject the invocation rather than choose among ambiguous inputs." The JS SDK only ever pairs activation withinput. The commit that added activation to its client also addedinput. Its server rejects an activated request that has noinput.Design
_invocation_arguments). The auth-discovery and signature-gate middleware parse the raw body without the request headers, then cache the result for dispatch. Because the member choice depends only on the invocation object, that cached parse and the executor always agree._profile_invocation_violation). The executor seesRequestContext.requested_extensions, which a2a-sdk fills from theA2A-Extensionsheader. The check reads member names from the protobufValuedirectly, so a non-finite number can't make it throw. It is skipped when the adopter supplies a custommessage_parser, because that parser owns its own wire shapes.context. Echoing it from either member would mean choosing between them.a2a_receipt_parameters, used forsync_reporting_receiptsandget_reporting_status) reads the same member. Before this change it also droppedinput.Verification
tests/test_a2a_profile_input_member.py: 17 tests that drive the real A2A app over HTTP. They cover activation by header and bymessage.extensions, 0.3 and 1.0parameters, both members with and without activation, each refusal shape, a custom parser, the signature gate's cached parse carryinginput, and the receipt raw-body path.74bbe6fac): 11 fail. Against this branch: all 17 pass.tests/test_a2a*.py,*auth*,*signed*,*receipt*,test_unified_mcp_a2a.py,test_seller_a2a_client.py): 1218 passed locally.@adcp/sdk@16.7.0 storyboard run <agent> capability_discovery --protocol a2a, served fromexamples/seller_agent.py'sDemoSellerover A2A.failing, with "Response echoes back the context object at /context" on both steps, the issue's exact symptom.media_buy_sellerover A2A with the same runner: 56 passed, 0 failed.🤖 Generated with Claude Code